<?php
namespace juven\libs {
    \defined('_JEXEC') or die('Restricted access');
    
    /**
     * @todo Sto callbacks_id na balw kai timestam
     */
    class EventsManager{
        
        const DEFAULT_CALLBACKS_MIN_PRIORITY        = 0;
        const DEFAULT_CALLBACKS_PRIORITY            = 128;
        const DEFAULT_CALLBACKS_MAX_PRIORITY        = 255;
        const DEFAULT_CALLBACKS_ID_PARTS_SEPARATOR  = "-";
        
        protected $_callbacksMinimumPriority;
        protected $_callbacksDefaultPriority;
        protected $_callbacksMaximumPriority;
        protected $_callbacksID_PartsSeparator;
        protected $_topics = array();
        
        /**
         * @param integer $priority_min
         * @param integer $defaultPriority
         * @param integer $priority_max
         * @param string $callbacksID_PartsSeparator
         * @return juven\libs\EventsManager          
         */
        public function __construct($callbacks_priority_min=self::DEFAULT_CALLBACKS_MIN_PRIORITY, 
                                    $callbacks_defaultPriority=self::DEFAULT_CALLBACKS_PRIORITY, 
                                    $callbacks_priority_max=self::DEFAULT_CALLBACKS_MAX_PRIORITY, 
                                    $callbacks_ID_PartsSeparator=self::DEFAULT_CALLBACKS_ID_PARTS_SEPARATOR){
            
            $this->_callbacksMinimumPriority = $callbacks_priority_min;
            $this->_callbacksDefaultPriority = $callbacks_defaultPriority;
            $this->_callbacksMaximumPriority = $callbacks_priority_max;
            $this->_callbacksID_PartsSeparator = $callbacks_ID_PartsSeparator;
            
            $this->load_static_eventHandlers();
        }
        
        protected function load_static_eventHandlers(){
            if (!\is_readable(juven::config()->eventsHandlersFolderPath)){
                @\error_log("Critical Error: Folder of static EventHandlers is not readable or does not exists.");
                return \FALSE;
            }
            
            foreach(glob(juven::config()->eventsHandlersFolderPath."*", GLOB_ONLYDIR) as $folder) { $directories[] = $folder; }
            var_dump($directories);
        }

        /**
         * if a callback has error, these errors are loged to apache error logs
         * 
         * @param string $topicName The name of the topic to publish
         * @param mix $eventData
         * @return boolean Returns TRUE if all callbacks have run, 
         *                         FALSE if a callback force to exit without running the remaining callbacks, 
         */
        public function publish($topicName, &$eventData=\NULL){ 
            if (!\array_key_exists($topicName, $this->_topics)){ $this->_topics[$topicName] = new EventTopic($topicName); }
            $dtp = new \stdClass();
            $dtp->topicName      = $topicName; 
            $dtp->eventData      = &$eventData; 
            $dtp->callerClass    = \NULL; 
            $dtp->callerFunction = \NULL;
            if (\version_compare(\PHP_VERSION, '5.4.0') >= 0){ $dt=\debug_backtrace(0,2);} else { $dt=\debug_backtrace(\FALSE); }
            if (\count($dt)>1){
                if (isset($dt[1]) && \array_key_exists("class",    $dt[1])){ $dtp->callerClass    = $dt[1]['class']; }
                if (isset($dt[1]) && \array_key_exists("function", $dt[1])){ $dtp->callerFunction = $dt[1]['function']; }
            }
              \ksort($this->_topics[$topicName]->callbacks);
            foreach ($this->_topics[$topicName]->callbacks as $prio) {
                foreach ($prio as $callb) {
                    try{
                        if ($callb->payload($dtp)===\FALSE){ return \FALSE; }
                    } catch (\Exception $e){
                        @\error_log("EventsManager->publish(): A callback failed to run: Exception: File [".@$e->getFile()."], Line:".@$e->getLine().", Message: ".@$e->getMessage());
                    }
                }
            }
            return \TRUE;
        }

        /**
         * @param string $topicName The topic name to subscribe to.
         * @param int $priority The priority of the callback
         * @param callable $callback A callable function/method, etc. to attach to $topicName
         * @return EventCallback Returns the registered EventCallback object.
         */
        public function subscribe($topicName, $priority=self::DEFAULT_CALLBACKS_PRIORITY, $callback=\NULL){
            if (!\array_key_exists($topicName, $this->_topics)){ $this->_topics[$topicName] = new EventTopic($topicName); }
            $prior = $priority;
            if (Tools::hasOnlyDigits($prior)!==\TRUE || \strlen((string)$prior)>6){ $prior = $this->_callbacksDefaultPriority; }
            if ($prior < $this->_callbacksMinimumPriority || $prior > $this->_callbacksMaximumPriority){ $prior = $this->_callbacksDefaultPriority; }
            
            if (!isset($this->_topics[$topicName]->callbacks[$prior])){ $this->_topics[$topicName]->callbacks[$prior] = array(); }
            $cb = new EventCallback(\is_callable($callback)?$callback:function(){}, $prior, $this->getNew_callbackID('n', $topicName, $prior), $this);
            $this->_topics[$topicName]->callbacks[$prior][] = $cb;
            return $cb;
        }
        
        /**
         * @param string $callback_id The (string)callback id
         * @return int Returns -1,-2,-3,-4,-5 if $callback_id has wrong format, 
         *                     -7 if $callback_id was not found 
         *                   or 1 if $callback_id was found and has been unset (unsubscribed).
         */
        public function unsubscribe_byID($callback_id){
            $p = \explode($this->_callbacksID_PartsSeparator, $callback_id);
            if ($p===\FALSE){ retrun -1;}
            if (!\is_array($p)){ return -2; }
            if (\count($p)!==4){ return -3; }
            if (!\array_key_exists($p[1], $this->_topics)){ return -4; }
            if (!\array_key_exists($p[2], $this->_topics[$p[1]]->callbacks)){ return -5; }
            foreach ($this->_topics[$p[1]]->callbacks[$p[2]] as $key => $value) {
                if ($value->get_id()===$callback_id){
                    unset($this->_topics[$p[1]]->callbacks[$p[2]][$key]);
                    return 1;
                }
            }
            return -7;
        }
        
        /** @return int  */
        public function getCallbacksDefaultPriority(){ return $this->_callbacksDefaultPriority; }
        
        /** @return int  */
        public function getCallbacksMinPriority(){ return $this->_callbacksMinimumPriority; }
        
        /** @return int  */
        public function getCallbacksMaxPriority(){ return $this->_callbacksMaximumPriority; }
        
        /** @return string  */
        public function getCallbacksID_PartsSeparator(){ return $this->_callbacksID_PartsSeparator; }
        
        /** @return array Returns All registered Topics */
        public function getTopics(){ return (array)$this->_topics; }
        
        /**
         * @param string $callback_id The (string)callback id
         * @return int|EventCallback Returns -1,-2,-3,-4,-5 if $callback_id has wrong format, 
         *                     -7 if $callback_id was not found 
         *                   or the EventCallback object with id = $callback_id.
         */
        public function getEventCallback_ByID($callback_id){
            $p = \explode($this->_callbacksID_PartsSeparator, $callback_id);
            if ($p===\FALSE){ retrun -1;}
            if (!\is_array($p)){ return -2; }
            if (\count($p)!==4){ return -3; }
            if (!\array_key_exists($p[1], $this->_topics)){ return -4; }
            if (!\array_key_exists($p[2], $this->_topics[$p[1]]->callbacks)){ return -5; }
            foreach ($this->_topics[$p[1]]->callbacks[$p[2]] as $key => $value) {
                if ($value->get_id()===$callback_id){ return $value; }
            }
            return -7;
        }
        
        /**
         * @todo Sto callbacks_id na balw kai timestam
         * @param string $topicType type of the topic (n=normal or s=stiky)
         * @param string $topicName
         * @param integer $priority
         * @return String  form = 'x1_x2_x3_x4', 
         *                      x1=type of the topic (n=normal or s=stiky)
         *                      x2=topicName
         *                      x3=priority number
         *                      x4=random chars
         *                      _ = callbacksID_PartsSeparator 
         */
        protected function getNew_callbackID($topicType, $topicName, $priority){
            return $topicType.$this->_callbacksID_PartsSeparator.$topicName.$this->_callbacksID_PartsSeparator.$priority.$this->_callbacksID_PartsSeparator.
            \preg_replace_callback('/[xy]/', 
                    function ($matches){
                        return \dechex('x' == $matches[0] ? \mt_rand(0, 15) : (\mt_rand(0, 15) & 0x3 | 0x8));
                    }, 'xxxxxxxxxxxxxxxx'
            );
        }
    }
    
    /**
     * Description of EventTopic
     *
     * @copyright Copyright (c) 2014, George Zeakis<zeageorge@gmail.com> 
     * @license http://www.gnu.org/licenses/gpl-3.0.txt GNU GENERAL PUBLIC LICENSE 
     *  
     * @author George Zeakis<zeageorge@gmail.com> 
     */
    class EventTopic {
        public $name;
        public $callbacks = array();
        public function __construct($name=\NULL){ $this->name = $name; }
    }
    
    /**
     * Description of EventCallback
     *
     * @copyright Copyright (c) 2014, George Zeakis<zeageorge@gmail.com> 
     * @license http://www.gnu.org/licenses/gpl-3.0.txt GNU GENERAL PUBLIC LICENSE 
     *  
     * @author George Zeakis<zeageorge@gmail.com> 
     */
    class EventCallback {
        /** @var string */
        protected $_id=\NULL;
        
        /** @var int */
        protected $_priority;
        
        /** @var callable */
        protected $_payload;
        
        /** @var EventsManager */
        protected $_eventsManager;

        /**
         * @param callable $payload A callable method,function,etc.
         * @param integer $priority
         * @param string $id
         */
        public function __construct(callable $payload, $priority, $id, EventsManager $em){
            $this->_payload  = $payload; 
            $this->_priority = $priority;
            $this->_id       = $id;
            $this->_eventsManager = $em;
        }

        /** @return int Returns the priority */
        public function get_priority(){ return $this->_priority; }
        
        /** @return string Returns the id */
        public function get_id(){ return $this->_id; }
        
        /** @return callable Returns the payload (function/method, etc.) */
        public function get_payload(){ return $this->_payload; }

        public function __call($name, $arguments) {
            if ($name=="payload"){
                // this must be this way or else we get child pid xxxx exit signal Segmentation fault (11)
                $r = &$this->_payload;
                return $r($arguments[0]);
            }
        }
        
        /**
         * @return int Returns -1,-2,-3,-4,-5 if EventCallback id has wrong format, 
         *                     -7 if EventCallback id was not found 
         *                   or 1 if EventCallback id was found and has been unset (unsubscribed).
         */
        public function unsubscribe(){ return $this->_eventsManager->unsubscribe_byID($this->_id); }
        
        /**
         * Registers to other Topic (event) with the same payload.
         * 
         * @param string $topicName The topic name to subscribe to.
         * @param int $priority The priority of the callback
         * @return EventCallback Returns the new registered EventCallback object.
         */
        public function subscribe($topicName, $priority=EventsManager::DEFAULT_CALLBACKS_PRIORITY){
            return $this->_eventsManager->subscribe($topicName, $priority, $this->_payload);
        }

        /**
         * Set the callback code
         * 
         * @param callable $payload
         * @return \juven\libs\EventCallback 
         */
        public function set_payload(callable $payload){ $this->_payload = $payload; return $this; }
        
        /**
         * 
         * @param string $name
         * @param mix $value
         * @return \juven\libs\EventCallback
         */
        public function __set($name, $value) { if ($name=="payload" && is_callable($value)){ $this->_payload = $value;  return $this; } }
    }
    
}
?>
